from types import ModuleType
from typing import Type, Any, Union, TypeVar, Literal, Callable, cast, Optional

import pytest
from httprunner import Config, Step, RunRequest, RunTestCase
from httprunner import exceptions
from httprunner.compat import ensure_testcase_v4
from httprunner.response import uniform_validator
from httprunner.step_request import RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation
from httprunner.step_testcase import StepRefCase

from ._runner import HttpRunnerForBuilder
from ._utils import validate_case_format, safe_class_name, create_debugtalk_parser

__all__ = ['easy_build', 'build']

T = TypeVar('T')
HOOK_FUN = Literal['setup_hook', 'teardown_hook']


def _build_config(config_dict: dict[str, Any]) -> Config:
    """构建 HttpRunner Config 对象"""
    config = Config(config_dict.get("name", "UnnamedTestCase"))

    if "variables" in config_dict:
        config = config.variables(**config_dict["variables"])
    if "base_url" in config_dict:
        config = config.base_url(config_dict["base_url"])
    if "verify" in config_dict:
        config = config.verify(config_dict["verify"])
    if "export" in config_dict:
        config = config.export(*config_dict["export"])

    return config


def _apply_hooks(obj: T, hook_fun: HOOK_FUN, hooks: list[Union[str, dict[str, str]]]) -> T:
    for hook in hooks:
        if isinstance(hook, str):
            obj = getattr(obj, hook_fun)(hook)
        elif isinstance(hook, dict) and len(hook) == 1:
            var_name, hook_content = list(hook.items())[0]
            obj = getattr(obj, hook_fun)(hook_content, var_name)
        else:
            raise exceptions.TestCaseFormatError(f"Invalid setup hook: {hook}")
    return obj


def _build_request(obj: RunRequest, req: dict[str, Any]) -> RequestWithOptionalArgs:
    method = req["method"].lower()
    rwa: RequestWithOptionalArgs = getattr(obj, method)(req["url"])

    if "params" in req:
        rwa = rwa.with_params(**req["params"])
    if "headers" in req:
        rwa = rwa.with_headers(**req["headers"])
    if "cookies" in req:
        rwa = rwa.with_cookies(**req["cookies"])
    if "data" in req:
        rwa = rwa.with_data(req["data"])
    if "json" in req:
        rwa = rwa.with_json(req["json"])
    if "timeout" in req:
        rwa = rwa.set_timeout(req["timeout"])
    if "verify" in req:
        rwa = rwa.set_verify(req["verify"])
    if "allow_redirects" in req:
        rwa = rwa.set_allow_redirects(req["allow_redirects"])
    if "upload" in req:
        rwa = rwa.upload(**req["upload"])

    return rwa


def _build_testcase_step(step: dict[str, Any], base_cls: Type[HttpRunnerForBuilder]) -> Step:
    base_step: RunTestCase = RunTestCase(step.get('name', 'UnnamedStep'))

    if 'variables' in step:
        base_step = base_step.with_variables(**step['variables'])
    if 'retry' in step:
        retry_times = step['retry'].get('retry_times', 0)
        retry_interval = step['retry'].get('retry_interval', 0)
        base_step = base_step.with_retry(retry_times, retry_interval)
    if 'setup_hooks' in step:
        base_step = _apply_hooks(base_step, 'setup_hook', step['setup_hooks'])

    sub_data = base_cls.load_testcase(step['testcase'])
    sub_cls = _build_httprunner_cls(sub_data, base_cls)
    ref: StepRefCase = base_step.call(sub_cls)

    if 'teardown_hooks' in step:
        ref = _apply_hooks(ref, 'teardown_hook', step['teardown_hooks'])

    if 'export' in step:
        ref = ref.export(*step['export'])

    return Step(ref)


def _build_request_validate(obj: Union[RequestWithOptionalArgs, StepRequestExtraction],
                            vs: list[dict[str, Any]]) -> StepRequestValidation:
    srv: StepRequestValidation = obj.validate()
    for v in vs:
        validator = uniform_validator(v)
        assert_method = validator['assert']
        check = validator['check']
        expect = validator['expect']
        message = validator['message']
        assert_func = getattr(srv, f'assert_{assert_method}')
        srv = assert_func(check, expect, message) if message else assert_func(check, expect)
    return srv


def _build_request_step(step: dict[str, Any]) -> Step:
    base_step: RunRequest = RunRequest(step.get('name', 'UnnamedStep'))

    if 'variables' in step:
        base_step = base_step.with_variables(**step['variables'])
    # todo retry并非官方文档的写法，后续需要按照官方文档的写法开发
    if 'retry' in step:
        retry_times = step['retry'].get('retry_times', 0)
        retry_interval = step['retry'].get('retry_interval', 0)
        base_step = base_step.with_retry(retry_times, retry_interval)
    if 'setup_hooks' in step:
        base_step = _apply_hooks(base_step, 'setup_hook', step['setup_hooks'])

    rwa: RequestWithOptionalArgs = _build_request(base_step, step['request'])

    if 'teardown_hooks' in step:
        rwa = _apply_hooks(rwa, 'teardown_hook', step['teardown_hooks'])

    final_step: Union[RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation] = rwa

    if 'extract' in step:
        sre: StepRequestExtraction = rwa.extract()
        for extract_name, extract_path in step["extract"].items():
            sre = sre.with_jmespath(extract_path, extract_name)
        final_step = sre

    if 'validate' in step:
        final_step = _build_request_validate(final_step, step['validate'])

    return Step(final_step)


def _build_step(step: dict[str, Any], base_cls: Type[T]) -> Step:
    if step.get("request"):
        return _build_request_step(step)
    elif step.get("testcase"):
        return _build_testcase_step(step, base_cls)
    else:
        raise exceptions.TestCaseFormatError(f'Invalid teststep: {step}')


def _build_test_start(skip: str = None, parameters: dict = None) -> Callable[..., Any]:
    # 基础 test_start_base 定义
    # test_start_base前面加_，因为pycharm会识别它为测试函数qaq
    def _test_start_base(self, *args, **kwargs):
        # 未来可在这里添加其他通用功能
        return super(self.__class__, self).test_start(*args, **kwargs)

    # 处理param参数
    if parameters:
        def _test_start(self, param):
            return _test_start_base(self, param)
    else:
        def _test_start(self):
            return _test_start_base(self)

    # 处理 skip 装饰器
    if skip is not None:
        reason = skip if skip else 'skip unconditionally'
        _test_start = pytest.mark.skip(reason=reason)(_test_start)

    # 处理参数化
    if parameters:
        from httprunner import Parameters
        _test_start = pytest.mark.parametrize("param", Parameters(parameters))(_test_start)

    return _test_start


def _build_httprunner_cls(data: Any, base_cls: Type[HttpRunnerForBuilder]) -> Type[HttpRunnerForBuilder]:
    # 0. 加载用例
    case: dict = base_cls.load_testcase(data)

    # 1. 兼容 v2/v3 -> v4
    case = ensure_testcase_v4(case)

    # 2. 校验 case 的格式
    validate_case_format(case)

    # 3. 构建 Config
    config_data = case['config']
    config = _build_config(config_data)

    # 4. 构建 Steps
    steps: list[Step] = [_build_step(s, base_cls) for s in case['teststeps']]

    # 5. 构建最终 HttpRunner 类
    class_name = safe_class_name(case['config'].get('name', 'Unnamed'))
    class_attrs: dict[str, Any] = {
        "config": config,
        "teststeps": steps,
        "test_start": _build_test_start(
            skip=config_data.get('skip', None),
            parameters=config_data.get('parameters', None)
        )
    }
    cls = type(class_name, (base_cls,), class_attrs)
    return cast(Type[T], cls)


def easy_build(
        data: Any,
        class_attr: Optional[dict[str, Any]] = None,
        debugtalk: Optional[Union[ModuleType, dict[str, Callable]]] = None,
        testcase_loader: Optional[Callable[[Any], dict]] = None,
) -> Type[HttpRunnerForBuilder]:
    """
    本质是build语法糖，参考build说明。
    """

    # 构建基类
    class HttpRunnerBase(HttpRunnerForBuilder):
        @staticmethod
        def load_testcase(_data: Any) -> dict:
            if testcase_loader is not None:
                return testcase_loader(_data)
            return HttpRunnerForBuilder.load_testcase(_data)

    # 添加类属性
    if class_attr is None:
        class_attr = {}
    if debugtalk is not None:
        class_attr['parser'] = create_debugtalk_parser(debugtalk)
    for attr, value in class_attr.items():
        setattr(HttpRunnerBase, attr, value)

    return build(data, HttpRunnerBase)


def build(
        data: Any,
        custom_cls: Optional[Type[HttpRunnerForBuilder]] = None
) -> Type[HttpRunnerForBuilder]:
    """
    通过自定义的基类构造httprunner测试类。注意：

    :param data: 任意数据，会被传入custom_cls.load_testcase解析
    :param custom_cls: 基类，必须是HttpRunnerForBuilder的子类
    :return: 返回一个HttpRunner子类，并已配置好teststeps和config等属性，可以直接调用test_start。

    1. 如果你要显示使用debugtalk，你必须要重载HttpRunnerForBuilder.parser。
    比如在你的类下面加 parser = Parser({"print_msg": print})或者可以通过工具函数create_debugtalk_parser(debugtalk)轻松创建。

    >>> import math
    >>>
    >>> class MyCustomRunner(HttpRunnerForBuilder):
    >>>     parser = Parser({"print_msg": print})
    >>>
    >>> class MyCustomRunner2(HttpRunnerForBuilder):
    >>>     parser = create_debugtalk_parser(math)

    2. 如果你要自定义用例数据加载方式，你必须要重载HttpRunnerForBuilder.load_testcase。
    你的数据要能够被成功转换成符合httprunner用例规范的dict。
    你可以通过重载该方法实现从任何地方加载数据，比如url/数据库/其他工具的数据。
    下面的例子可以从json/yaml文件路径和字典加载数据，这也是默认的实现。

    >>> class MyCustomRunner(HttpRunnerForBuilder):
    >>>     @staticmethod
    >>>     def load_testcase(data: Any) -> dict:
    >>>         if isinstance(data, dict):
    >>>             dict_data = deepcopy(data)
    >>>         elif isinstance(data, str):
    >>>             dict_data = load_test_file(data)
    >>>         else:
    >>>             raise exceptions.TestCaseFormatError(f"Invalid testcase: {data}")
    >>>         return dict_data
    """
    if custom_cls is None:
        custom_cls = HttpRunnerForBuilder
    return _build_httprunner_cls(data, custom_cls)
